/* Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above notice and this permission notice shall be included in all copies
 * or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
/* File for "Terrain" lesson of the OpenGL tutorial on
 * www.videotutorialsrock.com
 *
 *
 *
 *	edited by Jason Hsu (jh84), October 2009, Duke University
 *
 */





#ifndef TERRAIN_H_R45FD161
#define TERRAIN_H_R45FD161

#include "util/vec3f.h"
#include "util/color4f.h"
#include "imageloader.h"

#include <string>
#include <vector>
#include <iostream>


class Terrain {
private:
	GLint w;				//Width of terrain
	GLint l;				//Length (depth) of terrain
	float** hs;				//Heights
	Vec3f** normals;		//Normals
	bool computedNormals;	//Whether normals is up-to-date
	bool myCelShadingOn;	//Is cel-shading on?
	bool myOutlineOn;		//Is outlining on?
	GLfloat myLineWidth;
	Color4f myBaseColor;	//Base color of terrain
	
	GLfloat myHeightScale;	//Scales the height
	Vec3f mySunPos;
	struct tm* myTimeInfo;
	
public:
	
	/*
	 *	creates terrain, including heights parsed from
	 *	heightmap.  use loadBMP(file.bmp) to load heightmap
	 *	into Image* pointer.
	 *
	 */
	Terrain(GLint w2, GLint l2, Image* heightmap) {
		w = w2;
		l = l2;

		hs = new float*[l];				//a matrix representing the terrain (x,z) coordinates
		for(GLint i = 0; i < l; i++) {
			hs[i] = new float[w];
		}
		
		normals = new Vec3f*[l];		//a matrix representing the normals at each (x,z)
		for(GLint i = 0; i < l; i++) {
			normals[i] = new Vec3f[w];
		}
		
		myHeightScale = 15.0f;			//scales the height
		loadTerrain(heightmap);			//takes the heightmap and assigns heights at each (x,z)
		
		computedNormals = false;
		myCelShadingOn = true;
		myOutlineOn = true;
		myLineWidth = 3.0f;
		myBaseColor = Color4f(0.05f, 0.17f, 0.00f);
		
//		mySunPos = Scene::getSunPosition();
		
		myTimeInfo = Utility::getTime();
		GLfloat sun_pos_x = (5.0f/18.0f) * (myTimeInfo->tm_hour*60.0f + myTimeInfo->tm_min) - 200.0f;
		GLfloat sun_pos_y = -(1.0f/400.0f) * pow(sun_pos_x, 2) + 100.0f;
		mySunPos = Vec3f(sun_pos_x, sun_pos_y, 0);
	}
	
	~Terrain() {
		for(GLint i = 0; i < l; i++) {
			delete[] hs[i];
		}
		delete[] hs;
		
		for(GLint i = 0; i < l; i++) {
			delete[] normals[i];
		}
		delete[] normals;
	}
	
	GLint width() {
		return w;
	}
	
	GLint length() {
		return l;
	}
	
	//Sets the height at (x, z) to y
	void setHeight(GLint x, GLint z, GLfloat y) {
		hs[z][x] = y;
		computedNormals = false;
	}
	
	//Returns the height at (x, z)
	GLfloat getHeight(GLint x, GLint z) {
		return hs[z][x];
	}
	
	//Computes the normals, if they haven't been computed yet
	void computeNormals() {
		if (computedNormals) {
			return;
		}
		
		//Compute the rough version of the normals
		Vec3f** normals2 = new Vec3f*[l];
		for(GLint i = 0; i < l; i++) {
			normals2[i] = new Vec3f[w];
		}
		
		for(GLint z = 0; z < l; z++) {
			for(GLint x = 0; x < w; x++) {
				Vec3f sum(0.0f, 0.0f, 0.0f);
				
				Vec3f out;
				if (z > 0) {
					out = Vec3f(0.0f, hs[z - 1][x] - hs[z][x], -1.0f);
				}
				Vec3f in;
				if (z < l - 1) {
					in = Vec3f(0.0f, hs[z + 1][x] - hs[z][x], 1.0f);
				}
				Vec3f left;
				if (x > 0) {
					left = Vec3f(-1.0f, hs[z][x - 1] - hs[z][x], 0.0f);
				}
				Vec3f right;
				if (x < w - 1) {
					right = Vec3f(1.0f, hs[z][x + 1] - hs[z][x], 0.0f);
				}
				
				if (x > 0 && z > 0) {
					sum += out.cross(left).normalize();
				}
				if (x > 0 && z < l - 1) {
					sum += left.cross(in).normalize();
				}
				if (x < w - 1 && z < l - 1) {
					sum += in.cross(right).normalize();
				}
				if (x < w - 1 && z > 0) {
					sum += right.cross(out).normalize();
				}
				
				normals2[z][x] = sum;
			}
		}
		
		//Smooth out the normals
		const GLfloat FALLOUT_RATIO = 0.5f;
		for(GLint z = 0; z < l; z++) {
			for(GLint x = 0; x < w; x++) {
				Vec3f sum = normals2[z][x];
				
				if (x > 0) {
					sum += normals2[z][x - 1] * FALLOUT_RATIO;
				}
				if (x < w - 1) {
					sum += normals2[z][x + 1] * FALLOUT_RATIO;
				}
				if (z > 0) {
					sum += normals2[z - 1][x] * FALLOUT_RATIO;
				}
				if (z < l - 1) {
					sum += normals2[z + 1][x] * FALLOUT_RATIO;
				}
				
				if (sum.magnitude() == 0) {
					sum = Vec3f(0.0f, 1.0f, 0.0f);
				}
				normals[z][x] = sum;
			}
		}
		
		for(GLint i = 0; i < l; i++) {
			delete[] normals2[i];
		}
		delete[] normals2;
		
		computedNormals = true;
	}
	
	//Returns the normal at (x, z)
	Vec3f getNormal(GLint x, GLint z) {
		if (!computedNormals) {
			computeNormals();
		}
		return normals[z][x];
	}
	
	/*
	 *	sets heights of terrain according to heightmap passed in
	 *	as parameter.
	 *
	 */
	void loadTerrain(Image* heightmap) {
		for(GLint y = 0; y < heightmap->height; y++) {
			for(GLint x = 0; x < heightmap->width; x++) {
				unsigned char color = (unsigned char)heightmap->pixels[3 * (y * heightmap->width + x)];
				GLfloat h = myHeightScale*((color / 255.0f) - 0.5f);
				setHeight(x, y, h);
			}
		}
		computeNormals();
	}
	
	void draw()
	{
		glPushMatrix();

		GLfloat dotProduct, cellShadeOffset;							//dot product of light vector with normal vector
		Vec3f lightVector(mySunPos[0], mySunPos[1], mySunPos[2]);	//vector to the sun
		lightVector = lightVector.normalize();
		
		glEnable(GL_CULL_FACE);
		glEnable(GL_DEPTH_TEST);
		glEnable(GL_NORMALIZE);
		glShadeModel(GL_SMOOTH);
		
//		glMatrixMode(GL_MODELVIEW);
//		glLoadIdentity();
		
		glScalef(160.0/w, 1.0f, 1.0f);					//scales terrain to fit in window (approximate)
		glTranslatef(-(float)(w - 1) / 2,				//moves terrain in front of camera
					 0.0f,
					 -(float)(l - 1));
		
		glColor3f(0.18f, 0.30f, 0.13f);
		for(GLint z = 0; z < l - 1; z++) {		//draw terrain
			//Makes OpenGL draw a triangle at every three consecutive vertices
			glBegin(GL_TRIANGLE_STRIP);
			for(GLint x = 0; x < w; x++) {
				Vec3f normal = getNormal(x, z);
				if(!myCelShadingOn)
				{
					glNormal3f(normal[0], normal[1], normal[2]);
					glVertex3f(x, getHeight(x, z), z);

					normal = getNormal(x, z + 1);
					glNormal3f(normal[0], normal[1], normal[2]);
					glVertex3f(x, getHeight(x, z + 1), z + 1);
				}
				else 
				{
					dotProduct = (normal.normalize()).dot(lightVector);
					cellShadeOffset = getGradientVal(dotProduct);
					glColor3f(myBaseColor[0]+cellShadeOffset, myBaseColor[1]+cellShadeOffset, myBaseColor[2]+cellShadeOffset);
					glVertex3f(x, getHeight(x, z), z);

					normal = getNormal(x, z + 1);
					dotProduct = (normal.normalize()).dot(lightVector);
					cellShadeOffset = getGradientVal(dotProduct);
					glColor3f(myBaseColor[0]+cellShadeOffset, myBaseColor[1]+cellShadeOffset, myBaseColor[2]+cellShadeOffset);
					glVertex3f(x, getHeight(x, z + 1), z + 1);
				}
			}
			glEnd();
		}
		
		if(myOutlineOn)
		{
			Color4f greenOutline = Color4f(0.05f, 0.017f, 0.0f);
			Utility::enableOutlining(greenOutline, myLineWidth);
			for(GLint z = 0; z < l - 1; z++)						// Draw terrain frame with front faces culled
			{
				glBegin(GL_TRIANGLE_STRIP);
				for(GLint x = 0; x < w; x++) 
				{
					glVertex3f(x, getHeight(x, z), z);
					glVertex3f(x, getHeight(x, z + 1), z + 1);
				}
				glEnd();
			}
			Utility::disableOutlining();
		}
//		glutSwapBuffers();					// Don't know what this does.
		
		glPopMatrix();
	}
	
	void update()
	{
		GLfloat sun_pos_x = (5.0f/18.0f) * (myTimeInfo->tm_hour*60.0f + myTimeInfo->tm_min) - 200.0f;
		GLfloat sun_pos_y = -(1.0f/400.0f) * pow(sun_pos_x, 2) + 100.0f;
		mySunPos *= 0.0f;
		mySunPos += Vec3f(sun_pos_x, sun_pos_y, 0.0f);
	}
	
	GLfloat getGradientVal(GLfloat dotProduct)
	{
		if(dotProduct < 0.1954)
			return 0.05;
		else if(dotProduct < 0.5558)
			return 0.07;
		else if(dotProduct < 0.8316)
			return 0.09;
		else if(dotProduct < 0.9808)
			return 0.11;
		else
			return 0.13;
	}	
};



#endif /* end of include guard: BACKGROUND_H_R45FD161 */
